package com.acmen.easyapi.executor;

import com.acmen.easyapi.concurrent.TestCallable;
import com.acmen.easyapi.concurrent.TestProgress;
import com.acmen.easyapi.constant.AuthConstants;
import com.acmen.easyapi.dto.PressureTestCmd;
import com.acmen.easyapi.enums.BasicCode;
import com.acmen.easyapi.enums.TestMode;
import com.acmen.easyapi.factory.TestCallableFactory;
import com.acmen.easyapi.model.*;
import com.acmen.easyapi.repository.*;
import com.acmen.easyapi.util.ApiMenuTreeUtil;
import com.acmen.easyapi.util.RequestUtil;
import com.acmen.easyapi.util.StringUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.ui.ModelMap;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

/**
 * @author acmen
 * 组合测试执行器
 */
@Component
public class PressureTestExe {
    @Autowired
    private PressureTestRepository pressureTestRepository;
    @Autowired
    private TestReportRepository testReportRepository;
    @Autowired
    private CallResultRepository callResultRepository;

    @Autowired
    private UnitTestRepository unitTestRepository;
    @Autowired
    private AppRepository appRepository;
    @Autowired
    private ApiRepository apiRepository;
    @Autowired
    private TestCallableFactory testCallableFactory;
    @Autowired
    private EnvironmentRepository environmentRepository;
    @Value("${acmen.easyapi.intranetIp:}")
    private String intranetIp;

    private static Map<String, TestProgress> progressMap = new ConcurrentHashMap<>();

    public ModelAndView index(HttpServletRequest request, ModelMap model) {
        model.put("contextPath", request.getContextPath());
        String appName = request.getParameter("appName");
        App app = appRepository.selectByName(appName);
        List<Api> apis = apiRepository.selectListByAppName(appName);
        app.setApis(apis);
        model.put("app",app);
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : request.getCookies()) {//遍历cookie数组
                model.put(cookie.getName(), cookie.getValue());
            }
        }
        List<Environment> environments = environmentRepository.getAllByApp(appName);
        model.put("environments", environments);
        Set unitTestSet = unitTestRepository.queryForSetByApp(app.getName());
        List<Api> apiDetails = app.getApis().stream().filter(x->(unitTestSet.contains(x.getId()))).collect(Collectors.toList());
        model.put("menuTree", JSON.toJSONString(ApiMenuTreeUtil.getApiMenuTree(apiDetails)));

        ModelAndView view = new ModelAndView("/test/pressure-test", model);
        return view;
    }

    /**
     * 执行组合测试
     * @param pressureTestCmd
     * @param response
     * @throws InterruptedException
     * @throws IOException
     */
    public void exec(PressureTestCmd pressureTestCmd, HttpServletResponse response) throws InterruptedException, IOException {

        List<String> apiIds =  pressureTestCmd.getApiIds().stream().distinct().collect(Collectors.toList());
        String ip = RequestUtil.getIpAddress();
        if(!(ip.matches(intranetIp.replace("*",".*"))||ip.startsWith("127.0."))){
            List<Environment> environments = environmentRepository.getGatewayEnvironments(pressureTestCmd.getAppName());
            boolean pass = environments.stream().anyMatch(environment -> environment.getBasePath().equals(pressureTestCmd.getBasePath()));
            //如果不是内网IP调用，只能调网关地址
            if(!pass){
                progressMap.put(pressureTestCmd.getTestId(),new TestProgress(true));
                Response res = Response.failure(BasicCode.FORBIDDEN.getCode(),"你的网络不是内网IP，只能调网关地址！");
                response.getWriter().println(JSON.toJSONString(res));
                return;
            }
        }
        int concurrency = pressureTestCmd.getConcurrency();
        int number = pressureTestCmd.getNumber();
        Map headers = getHeaders(pressureTestCmd);
        App app = appRepository.selectByName(pressureTestCmd.getAppName());
        List<Api> apis = apiRepository.selectListByIds(apiIds);
        app.setApis(apis);
        List<TestCallable> tasks = testCallableFactory.createCallables(apis,pressureTestCmd.getBasePath(), headers, concurrency*number);
        String testName = null;
        if(TestMode.UNORDERED.getCode().equals(pressureTestCmd.getMode())){
            Random random = new Random();
            List<TestCallable> tempTasks = new ArrayList<>();
            while (!tasks.isEmpty()){
                int idx = random.nextInt(tasks.size());
                tempTasks.add(tasks.remove(idx));
            }
            tasks = tempTasks;
            testName = concurrency + "个并发"+number+"次所有接口无序组合测试(" + pressureTestCmd.getEnv().toUpperCase() + ")";
        }else{
            testName = concurrency + "个并发"+number+"次按接口有序组合测试(" + pressureTestCmd.getEnv().toUpperCase() + ")";
        }
        TestProgress testProgress = new TestProgress(tasks);
        progressMap.put(pressureTestCmd.getTestId(),testProgress);
        //创建线程池
        ExecutorService threadPoolExecutor = new ThreadPoolExecutor(concurrency, concurrency, 0L,
                TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1000000),
                new ThreadPoolExecutor.CallerRunsPolicy());
        try{
            List<Future<CallResult>> futures = threadPoolExecutor.invokeAll(tasks);
            processExecResult(app,apis,pressureTestCmd.getBasePath(), headers,concurrency,testName, futures);
            Response res = Response.success();
            response.getWriter().println(JSON.toJSONString(res));
        }finally {
            threadPoolExecutor.shutdown();
            testProgress.getTasks().clear();
            testProgress.setCompleted(true);
        }
    }

    /**
     * 组合测试进度
     * @param request
     * @param response
     * @throws IOException
     */
    public void progress(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String testId = request.getParameter("testId");
        TestProgress progress = progressMap.get(testId);
        JSONObject res = new JSONObject();
        if(progress==null){
            res.put("code","0");
        }else if(progress.isCompleted()==false){
            res.put("code","1");
            int count = 0;
            for(TestCallable task : progress.getTasks()){
                if(task.isCompleted()){
                    count ++;
                }
            }
            int completion = count*100/progress.getTasks().size();
            if(completion==100){
                res.put("code","2");
            }else{
                res.put("code","1");
            }
            res.put("completion",completion);
        }else{
            res.put("code","3");
        }
        response.getWriter().println(JSON.toJSONString(res));
    }


    public Map<String, String> getHeaders(PressureTestCmd pressureTestCmd) {
        Map headers = new HashMap();
        if (StringUtil.isNotEmpty(pressureTestCmd.getAccessToken())) {
            headers.put(AuthConstants.ACCESS_TOKEN,pressureTestCmd.getAccessToken());
        }
        if(StringUtil.isNotEmpty(pressureTestCmd.getJwt())){
            headers.put(AuthConstants.JWT,pressureTestCmd.getJwt());
        }
        return headers;
    }

    /**
     * 处理测试结果
     *
     * @param basePath
     * @param concurrency
     * @param headers
     * @param app
     * @param futures
     */
    public PressureTest processExecResult(App app, List<Api> apis, String basePath, Map<String, String> headers, int concurrency, String testName, List<Future<CallResult>> futures)
            throws InterruptedException {
        Map<String, TestReport> aggregateReports = new HashMap<>();
        List<CallResult> callResults = new ArrayList<>();
        for (Future<CallResult> future : futures) {
            try {
                CallResult callResult = future.get();
                if (!aggregateReports.containsKey(callResult.getApiId())) {
                    Api api = apis.stream().filter(e->e.getId().equals(callResult.getApiId())).findFirst().orElse(null);
                    if(api == null){
                        continue;
                    }
                    TestReport report = new TestReport();
                    report.setId(UUID.randomUUID().toString().replace("-", ""));
                    report.setApiId(api.getId());
                    report.setApiName(api.getName());
                    report.setPath(api.getPath());
                    report.setMethod(api.getRequestMethod());
                    aggregateReports.put(callResult.getApiId(), report);
                    callResult.setReportId(report.getId());
                }
                TestReport report = aggregateReports.get(callResult.getApiId());
                callResult.setReportId(report.getId());
                report.setTotal(report.getTotal() + 1);
                report.setTotalCost(report.getTotalCost() + callResult.getCost());
                if (report.getMinCost() == -1 || report.getMinCost() > callResult.getCost()) {
                    report.setMinCost(callResult.getCost());
                }
                if (report.getMaxCost() < callResult.getCost()) {
                    report.setMaxCost(callResult.getCost());
                }
                report.setSendSize(report.getSendSize() + callResult.getSendSize());
                report.setReceiveSize(report.getReceiveSize() + callResult.getReceiveSize());
                if (callResult.isResponseSuccess()) {
                    report.setResponseSuccessNum(report.getResponseSuccessNum() + 1);
                }
                if (callResult.isBusinessSuccess()) {
                    report.setBusinessSuccessNum(report.getBusinessSuccessNum() + 1);
                }
                callResults.add(callResult);
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
        aggregateReports.forEach((apiId, report) -> {
            report.setAvgCost(report.getTotalCost() / report.getTotal());
        });
        PressureTest pressureTest = new PressureTest();
        pressureTest.setAppName(app.getName());
        pressureTest.setBasePath(basePath);
        pressureTest.setCreateDt(LocalDateTime.now());
        pressureTest.setId(UUID.randomUUID().toString().replace("-", ""));
        pressureTest.setName(testName);
        pressureTest.setHeaders(headers);
        pressureTest.setConcurrency(concurrency);
        List<TestReport> reportList = new ArrayList<>();
        aggregateReports.forEach((apiId, report) -> {
            report.setTestId(pressureTest.getId());
            reportList.add(report);
        });
        pressureTest.setAggregateReports(reportList);
        boolean insertResult = pressureTestRepository.save(pressureTest);
        if (insertResult) {
            testReportRepository.batchCreate(reportList);
            callResultRepository.batchCreate(callResults);
        }
        return pressureTest;
    }

    public boolean delete(String id) {
        pressureTestRepository.delete(id);
        List<TestReport> aggregateReports = testReportRepository.selectList(id);
        aggregateReports.forEach(aggregateReport -> {
            testReportRepository.delete(aggregateReport.getId());
            callResultRepository.deleteByReportId(aggregateReport.getId());
        });
        return true;
    }

    public List<PressureTest> selectListByAppName(String appName) {
        List<PressureTest> pressureTests = pressureTestRepository.selectListByAppName(appName);
        return pressureTests;
    }

    public PressureTest getById(String id) {
        PressureTest pressureTest = pressureTestRepository.selectOne(id);
        pressureTest.setAggregateReports(testReportRepository.selectList(id));
        return pressureTest;
    }

    @Scheduled(fixedRate=1000*60*60)
    public void clearTimeoutProgress(){
        long now = System.currentTimeMillis();
        List<String> keys = new ArrayList<>();
        progressMap.forEach((k,v)->{
            if(now-v.getCreateDt()>1000*60*60){
                keys.add(k);
            }
        });
        keys.forEach(key->{
            progressMap.remove(key);
        });
    }
}
